
/**
 * final修饰符
 * final关键字可用于修饰类、变量和方法，用于表示它修饰的类、方法和变量不可改变。
 * final修饰变量时，表示该变量一旦获得了初始值就不可被改变，final既可以修饰成员变量（包括类变量和实例变量），也可以修饰局部变量、形参。
 */

import java.util.Arrays;
import java.util.Objects;

/**
 * final成员变量
 * Java语法规定：final修饰的成员变量必须由程序员显式地指定初始值。
 * final修饰的类变量、实例变量能指定初始值的地方如下。
 * ➢ 类变量：必须在静态初始化块中指定初始值或声明该类变量时指定初始值，而且只能在两个地方的其中之一指定。
 * ➢ 实例变量：必须在非静态初始化块、声明该实例变量或构造器中指定初始值，而且只能在三个地方的其中之一指定。
 * 尽量避免在final成员变量显式初始化之前访问它。
 */
class FinalVariableTest {
    // 定义成员变量时指定默认值
    final int a = 6;
    // 下面变量将在构造器或初始化中分配初始值
    final String str;
    final int c;
    final static double d;

    //
    {
        str = "Hello";
    }

    static {
        d = 5.6;
    }

    public FinalVariableTest() {
        c = 5;
    }
}

/**
 * final局部变量
 * <p>
 * 系统不会对局部变量进行初始化，局部变量必须由程序员显式初始化。
 * 因此使用final修饰局部变量时，既可以在定义时指定默认值，也可以不指定默认值。
 * 如果final修饰的局部变量在定义时没有指定默认值，则可以在后面代码中对该final变量赋初始值，但只能一次，不能重复赋值；
 * 如果final修饰的局部变量在定义时已经指定默认值，则后面代码中不能再对该变量赋值。
 */
class FinalLocalVariableTest {
    // 因为形参在调用该方法时，由系统根据传入的参数来完成初始化，因此使用final修饰的形参不能被赋值。
    public void test(final int a) {
        // 不能对final修饰的形参赋值，
//        a = 5;
    }

    public void test2() {
        // 定义final局部变量时指定默认值，则str变量无法重新赋值
        final String str = "hello";
//        str = "Java";
        // 定义final局部变量时没有指定默认值，则d变量可以被赋值一次
        final double d;
        d = 5.6;
//        d = 3.4;
    }
}

/**
 * final修饰基本类型变量和引用类型变量的区别
 * 当使用final修饰基本类型变量时，不能对基本类型变量重新赋值，因此基本类型变量不能被改变。
 * 但对于引用类型变量而言，它保存的仅仅是一个引用，final只保证这个引用类型变量所引用的地址不会改变，即一直引用同一个对象，但这个对象完全可以发生改变。
 */
class Person {
    private int age;

    public Person() {
    }

    public Person(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

class FinalReferenceTest {
    void test() {
        final int[] iArr = {5, 6, 12, 9};
        System.out.println(Arrays.toString(iArr));
        Arrays.sort(iArr);
        System.out.println(Arrays.toString(iArr));
        iArr[2] = -8;
        System.out.println(Arrays.toString(iArr));
//        iArr = null;  // 重新赋值，非法
        final Person person = new Person(45);
        person.setAge(23);
        System.out.println(person.getAge());
//        person = null;    // 重新赋值，非法

        // 从上面程序中可以看出，使用final修饰的引用类型变量不能被重新赋值，但可以改变引用类型变量所引用对象的内容。
    }
}

/**
 * 可执行“宏替换”的final变量
 * 对一个final变量来说，不管它是类变量、实例变量，还是局部变量，
 * 只要该变量满足三个条件，这个final变量就不再是一个变量，而是相当于一个直接量。
 * ➢ 使用final修饰符修饰。
 * ➢ 在定义该final变量时指定了初始值。
 * ➢ 该初始值可以在编译时就被确定下来。
 */
class FinalLocalTest {
    // 对于这个程序来说，变量a其实根本不存在，当程序执行System.out.println(a);
    // 代码时，实际转换为执行System.out.println(5)。
    public void test() {
        final int a = 5;
        System.out.println(a);
    }

    // 除上面那种为final变量赋值时赋直接量的情况外，
    // 如果被赋的表达式只是基本的算术表达式或字符串连接运算，没有访问普通变量，调用方法，Java编译器同样会将这种final变量当成“宏变量”处理。
    // 查看编译后的.class文件也可以得知
    public void test2() {
        // 下面的都是final宏变量
        final int a = 5 + 2;
        final double b = 1.2 / 3;
        final String str = "学习" + "Java";
        final String book = "Java从入门到精通: " + 65;
        // 下面的book2变量因为调用的了方法，所以无法在编译时被确定下来
        final String book2 = "Java从入门到精通: " + String.valueOf(65);
    }
}

/**
 * final修饰的方法不可被重写，如果出于某些原因，不希望子类重写父类的某个方法，则可以使用final修饰该方法。
 */
class FinalMethodTest {
    public final void test() {
    }

    /**
     * 对于一个private方法，因为它仅在当前类中可见，其子类无法访问该方法，
     * 所以子类无法重写该方法—如果子类中定义一个与父类private方法有相同方法名、相同形参列表、相同返回值类型的方法，
     * 也不是方法重写，只是重新定义了一个新方法。
     * 因此，即使使用final修饰一个private访问权限的方法，依然可以在其子类中定义与该方法具有相同方法名、相同形参列表、相同返回值类型的方法。
     * 因此，使用final修饰private方法没有任何意义
     */
    private final void test2() {
    }

}

class FinalMethodTestSub extends FinalMethodTest {
    // 编译错误，不能重写父类final方法
//    public void test() {
//    }
    // 不是重写父类的方法
    public void test2() {
    }


    // final修饰的方法仅仅是不能被重写，并不是不能被重载，因此下面程序完全没有问题。
    public void test(String arg) {

    }
}

/**
 * final类
 * final修饰的类不可以有子类，例如java.lang.Math类就是一个final类，它不可以有子类。
 * 当子类继承父类时，将可以访问到父类内部数据，并可通过重写父类方法来改变父类方法的实现细节，这可能导致一些不安全的因素。
 * 为了保证某个类不可被继承，则可以使用final修饰这个类。
 */

final class FinalClass {

}

// 编译错误
//class FinalClassSub extends FinalClass {
//
//}

/**
 * 不可变类
 * 不可变（immutable）类的意思是创建该类的实例后，该实例的实例变量是不可改变的。
 * Java提供的8个包装类和java.lang.String类都是不可变类，当创建它们的实例后，其实例的实例变量不可改变。
 * 如果需要创建自定义的不可变类，可遵守如下规则。
 * ➢ 使用private和final修饰符来修饰该类的成员变量。
 * ➢ 提供带参数的构造器（或返回该实例的类方法），用于根据传入参数来初始化类里的成员变量。
 * ➢ 仅为该类的成员变量提供getter方法，不要为该类的成员变量提供setter方法，因为普通方法无法修改final修饰的成员变量。
 * ➢ 如果有必要，重写Object类的hashCode()和equals()方法。equals()方法根据关键成员变量来作为两个对象是否相等的标准，
 * 除此之外，还应该保证两个用equals()方法判断为相等的对象的hashCode()也相等。
 */
// 下面定义一个不可变的Address类，程序把Address类的detail和postCode成员变量都使用private隐藏起来，
// 并使用final修饰这两个成员变量，不允许其他方法修改这两个成员变量的值。
class Address {
    private final String detail;
    private final String postCode;

    public Address(String detail, String postCode) {
        this.detail = detail;
        this.postCode = postCode;
    }

    public String getDetail() {
        return detail;
    }

    public String getPostCode() {
        return postCode;
    }

    /**
     * 重写equals方法，判断两个对象是否相等
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Address address = (Address) o;
        return Objects.equals(detail, address.detail) &&
                Objects.equals(postCode, address.postCode);
    }

    /**
     * 确保同一个对象的hashCode是相同的
     */
    @Override
    public int hashCode() {
        return Objects.hash(detail, postCode);
    }
}

/**
 * 当使用final修饰引用类型变量时，仅表示这个引用类型变量不可被重新赋值，但引用类型变量所指向的对象依然可改变。
 * 这就产生了一个问题：当创建不可变类时，如果它包含成员变量的类型是可变的，那么其对象的成员变量的值依然是可改变的—这个不可变类其实是失败的。
 * 下面程序试图定义一个不可变的Person类，但因为Person类包含一个引用类型的成员变量，且这个引用类是可变类，所以导致Person类也变成了可变类。
 */
class Name {
    private String firstName;
    private String lastName;

    public Name() {
    }

    public Name(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}

class Person2 {
    private final Name name;

    public Person2(Name name) {
        this.name = name;
    }

    public Name getName() {
        return name;
    }

    public static void test() {
        Name n = new Name("悟空", "孙");
        Person2 p = new Person2(n);
        System.out.println(p.getName().getFirstName());
        // 改变Person对象Name的firstName的值
        n.setFirstName("八戒");
        System.out.println(p.getName().getFirstName());
    }
}

/**
 * 为了保持Person对象的不可变性，必须保护好Person对象的引用类型的成员变量：name，
 * 让程序无法访问到Person对象的name成员变量，也就无法利用name成员变量的可变性来改变Person对象了。
 * 为此将Person类改为如下：
 */
class Person3 {
    private final Name name;

    public Person3(Name name) {
        // 保存的并不是传入的对象
        this.name = new Name(name.getFirstName(), name.getLastName());
    }

    public Name getName() {
        // 也不能返回自己存的对象
        return new Name(name.getFirstName(), name.getLastName());
    }

    public static void test() {
        Name n = new Name("悟空", "孙");
        Person3 p = new Person3(n);
        System.out.println(p.getName().getFirstName());
        // 改变Person对象Name的firstName的值
        n.setFirstName("八戒");
        System.out.println(p.getName().getFirstName());
    }
}

public class FinalTest {
    public static void main(String[] args) {
        Person2.test();
        Person3.test();
    }
}
